package com.brightworks.scripthelper.analyzers
{
    import com.brightworks.scripthelper.constants.LanguageCodeConstants;
    import com.brightworks.scripthelper.constants.LineTypeConstants;
    import com.brightworks.scripthelper.constants.MiscConstants;
    import com.brightworks.scripthelper.model.VoiceScriptsInfo;
    import com.brightworks.scripthelper.model.VoiceScriptsLanguageInfo;
    import com.brightworks.scripthelper.model.VoiceScriptsRoleInfo;
    import com.brightworks.util.Log;
    import com.brightworks.util.Utils_DataConversionComparison;
    import com.brightworks.util.Utils_String;

    import flash.utils.Dictionary;

    public class ScriptAnalyzer
    {
        public var roleList:Array; // an Array of strings - roles defined in header roles line

        private var _bProblemsWithRoles:Boolean;
        private var _chunkAnalyzerList:Array; // contains ChunkAnalyzer instances
        private var _chunkAnalyzersSortedByRole:Dictionary;
        private var _chunkCount:int;
        private var _currentChunkLineTypes:Array;
        private var _currentHeaderLineTypes:Array;
        private var _headerLineCount:int = 11;
        private var _headerLineNumber_AuthorName:int = 1;
        private var _headerLineNumber_LessonId:int = 4;
        private var _headerLineNumber_LessonName:int = 2;
        private var _headerLineNumber_LessonSortName:int = 3;
        private var _headerLineNumber_Level:int = 9;
        private var _headerLineNumber_LibraryId:int = 6;
        private var _headerLineNumber_NativeLanguage:int = 7;
        private var _headerLineNumber_ProviderId:int = 5;
        private var _headerLineNumber_ReleaseType:int = 11;
        private var _headerLineNumber_Roles:int = 10;
        private var _headerLineNumber_TargetLanguage:int = 8;
        private var _lessonName:String;
        private var _lineAnalyzerClassIndex:Dictionary;
        private var _lineAnalyzerList:Array; // contains LineTypeAnalyzer instances
        private var _lineCount:int;
        private var _lineStringList:Array; // an Array of strings, one for each line
        private var _nativeLanguageCount:int = 1; // Hard-coded for now
        private var _payment__chinese__yuan_per_syllable:Number = .0525; // Hard-coded for now
        private var _payment__english__dollars_per_code_char:Number = .01; // Hard-coded for now
        private var _payment__english__dollars_per_word:Number = .025; // Hard-coded for now
        private var _problemsList:Array; // an Array of strings - "problem" results
        private var _releaseStatementEnglishWordCount:int = 35; // Hard-coded for now
        private var _releaseStatementHanziCharacterCount:int = 43; // Hard-coded for now
        private var _resultsList:Array; // an Array of strings - results that aren't problems
        [Bindable]
        private var _resultsString:String;
        private var _scriptText:String;

        // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
        //
        //          Public Methods
        //
        // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

        public function ScriptAnalyzer()
        {
            initProps();
        }

        public function analyze(scriptText:String):String
        {
            // If we can assume that we have a blank line at the end of our script it
            // simplifies our code. See analyzeChunks_CreateChunkAnalyzerList in 
            // particular - it checks to see if it has "chunk data" from previous lines
            // on each non-chunk line. If there is no non-chunk line after the last
            // chunk it doesn't create a ChunkAnalyzer instance for the last chunk.
            scriptText += "\n"
            analyze_InitProps(scriptText);
            // The next method checks each line and makes an initial determination of it's
            // type based on its position within the script, etc. We don't check the line's
            // contents at this point so this may not really identify what type of line the
            // line is. We may or may not check all of the lines - for example, if the script
            // doesn't have the correct number of header lines we abort. Once each line passes
            // this initial check a LineTypeAnalyzer instance is created and added to 
            // _lineAnalyzerList.
            analyzeLines_CreateLineAnalyzerList();
            if (doProblemReportsExist())
            {
                reportResults();
                return _resultsString;
            }
            populateRolesListProp();
            populateLessonNameProp();
            // The next method checks each line to see whether its contents are appropriate for 
            // its type.
            analyzeLines_CheckLines();
            if (doProblemReportsExist())
            {
                reportResults();
                return _resultsString;
            }
            analyzeLines_SetRoleInfo();
            if (doProblemReportsExist())
            {
                reportResults();
                return _resultsString;
            }
            analyzeChunks_CreateChunkAnalyzerLists();
            analyzeChunks_CheckChunks();
            analyzeChunks_CheckChunkRelationships();
            reportResults();
            return _resultsString;
        }

        public function createLessonXML():XML
        {
            var result:XML = <lesson/>;
            result.appendChild(createXML_ChunksElement());
            return result;
        }

        public function createVoiceScriptsInfo():VoiceScriptsInfo
        {
            if (doProblemReportsExist())
                Log.fatal("ScriptAnalyzer.createVoiceScriptsInfo(): Problem reports exist.");
            var result:VoiceScriptsInfo = new VoiceScriptsInfo();
            result.languageInfoList = new Dictionary();
            result.languageInfoList[LanguageCodeConstants.ISO_639_3_CODE_CHINESE_MANDARIN] = createVoiceScriptsInfo_CreateVoiceScriptsLanguageInfo_Chinese();
            result.languageInfoList[LanguageCodeConstants.ISO_639_3_CODE_ENGLISH] = createVoiceScriptsInfo_CreateVoiceScriptsLanguageInfo_English();
            return result;
        }

        public function doProblemReportsExist():Boolean
        {
            return ((_problemsList) && (_problemsList.length > 0));
        }

        // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
        //
        //          Private Methods
        //
        // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

        private function addChunkAnalyzerToChunkAnalyzersSortedByRole(analyzer:ChunkAnalyzer):void
        {
            var roleName:String = analyzer.roleName;
            if (!roleName)
                roleName = MiscConstants.ROLE_DEFAULT;
            if (!_chunkAnalyzersSortedByRole.hasOwnProperty(roleName))
                _chunkAnalyzersSortedByRole[roleName] = [];
            (_chunkAnalyzersSortedByRole[roleName] as Array).push(analyzer);
        }

        private function addProblemMessage(problemString:String):void
        {
            _problemsList.push(problemString);
        }

        private function addProblemMessages(problemList:Array):void
        {
            for each (var problemString:String in problemList)
            {
                addProblemMessage(problemString);
            }
        }

        private function analyze_InitProps(scriptText:String):void
        {
            _scriptText = scriptText;
            _bProblemsWithRoles = false;
            _currentChunkLineTypes = [];
            _currentHeaderLineTypes = [];
            _lineAnalyzerList = [];
            _lineStringList = Utils_DataConversionComparison.convertStringToArrayOfLineStrings(_scriptText, true);
            _lineCount = _lineStringList.length;
            _problemsList = [];
            _resultsString = "";
            _resultsList = [];
        }

        private function analyzeChunks_CheckChunkRelationships():void
        {
            var problemString:String;
            for (var roleName:String in _chunkAnalyzersSortedByRole)
            {
                var chunkListForRole:Array = _chunkAnalyzersSortedByRole[roleName];
                var analyzerCount:int = chunkListForRole.length;
                for (var i:int = 0; i < (analyzerCount - 1); i++)
                {
                    var firstAnalyzerInPair:ChunkAnalyzer = chunkListForRole[i];
                    var secondAnalyzerInPair:ChunkAnalyzer = chunkListForRole[i + 1];
                    if (secondAnalyzerInPair.isLineProcededByIgnoreProblemsCommentLine(LineTypeConstants.LINE_TYPE_ID__CHUNK__TARGET_LANGUAGE__ROMANIZED))
                        continue;
                    if (firstAnalyzerInPair.isPinyinLineFinalOrOnlyChunkInSentence())
                    {
                        if (!secondAnalyzerInPair.isPinyinLineFirstOrOnlyChunkInSentence())
                        {
                            var secondPinyinLineLineNumber:int = secondAnalyzerInPair.getLineNumber(LineTypeConstants.LINE_TYPE_ID__CHUNK__TARGET_LANGUAGE__ROMANIZED);
                            var secondPinyinLineLineText:String = secondAnalyzerInPair.getLineText(LineTypeConstants.LINE_TYPE_ID__CHUNK__TARGET_LANGUAGE__ROMANIZED);
                            problemString = "The previous pinyin line for the '" + roleName + "' role appears to be the end of a sentence, so this pinyin line should start with a capital letter.";
                            addProblemMessages([constructFullProblemString(secondPinyinLineLineNumber, secondPinyinLineLineText, problemString)]);
                        }
                    }
                }
            }
        }

        private function analyzeChunks_CheckChunks():void
        {
            var chunkAnalyzer:ChunkAnalyzer;
            for (var i:int = 0; i < _chunkCount; i++)
            {
                chunkAnalyzer = _chunkAnalyzerList[i];
                addProblemMessages(chunkAnalyzer.getProblemReports());
            }
        }

        private function analyzeChunks_CreateChunkAnalyzerLists():void
        {
            _chunkAnalyzerList = [];
            _chunkAnalyzersSortedByRole = new Dictionary();
            var currentChunkAndCommentLineAnalyzerList:Array = [];
            var newChunkAnalyzer:ChunkAnalyzer;
            var currChunkNumber:int = 0;
            var currRoleName:String;
            for each (var lineAnalyzer:LineTypeAnalyzer in _lineAnalyzerList)
            {
                if ((lineAnalyzer.bIsChunkLine) || (lineAnalyzer is LineTypeAnalyzer_Comment))
                {
                    currentChunkAndCommentLineAnalyzerList.push(lineAnalyzer);
                }
                else
                {
                    // Not a chunk line - do we have a chunk's data to process?
                    switch (computeNumberOfChunkLineAnalyzersInList(currentChunkAndCommentLineAnalyzerList))
                    {
                        case 0:
                            // Do nothing - no data to process
                            break;
                        case 3:
                        case 4:
                            // Yes, we have data to process. Create a ChunkAnalyzer instance.
                            currChunkNumber ++;
                            newChunkAnalyzer = new ChunkAnalyzer();
                            newChunkAnalyzer.chunkNumber = currChunkNumber;
                            newChunkAnalyzer.lineCode = Utils_String.padBeginning(String(currChunkNumber), 2, "0");
                            if (currRoleName)
                            {
                                newChunkAnalyzer.roleName = currRoleName;
                            }
                            else
                            {
                                if (doesScriptUseDefaultRole())
                                {
                                    newChunkAnalyzer.roleName = MiscConstants.ROLE_DEFAULT;
                                }
                                else
                                {
                                    Log.fatal("ScriptAnalyzer.analyzeChunks_CreateChunkAnalyzerList():  Chunk line occurs before role any role identification line - this should have been caught in 'analysis' stage and should have prevented 'create scripts' stage");
                                }
                            }
                            for each (var subLoopLineAnalyzer:LineTypeAnalyzer in currentChunkAndCommentLineAnalyzerList)
                            {
                                if (subLoopLineAnalyzer.bIsChunkLine)
                                    newChunkAnalyzer.addLineTypeAnalyzer(subLoopLineAnalyzer);
                            }
                            newChunkAnalyzer.lineAnalyzerListIncludingCommentLines = currentChunkAndCommentLineAnalyzerList;
                            _chunkAnalyzerList.push(newChunkAnalyzer);
                            addChunkAnalyzerToChunkAnalyzersSortedByRole(newChunkAnalyzer);
                            // Reset list, ready to gather data for a next chunk
                            currentChunkAndCommentLineAnalyzerList = [];
                            break;
                        default:
                            Log.fatal("ScriptAnalyzer.analyzeChunks_CreateChunkAnalyzerList(): " + currentChunkAndCommentLineAnalyzerList.length + "- should always be 3 or 4");
                    }
                    if (lineAnalyzer is LineTypeAnalyzer_RoleIdentification)
                    {
                        currRoleName = LineTypeAnalyzer_RoleIdentification(lineAnalyzer).roleName;
                    }
                }
            }
            _chunkCount = _chunkAnalyzerList.length;
        }

        private function analyzeLines_CheckLines():void
        {
            var lineAnalyzer:LineTypeAnalyzer;
            for (var i:int = 0; i < _lineCount; i++)
            {
                lineAnalyzer = _lineAnalyzerList[i];
                switch (lineAnalyzer.lineTypeId)
                {
                    case LineTypeConstants.LINE_TYPE_ID__ROLE_IDENTIFICATION:
                        LineTypeAnalyzer_RoleIdentification(lineAnalyzer).rolesList = roleList;
                        break;
                }
                if (!isLineIgnoreProblemsCommentLine(i))
                {
                    addProblemMessages(lineAnalyzer.getProblemReports());
                }
            }
        }

        private function analyzeLines_CreateLineAnalyzerList():void
        {
            if (!doesScriptHaveAnyBlankLines())
            {
                createProblemMessage_ScriptHasNoBlankLines();
                return;
            }
            if (getFirstBlankLineNumAfter(0) == 1)
            {
                createProblemMessage_EmptyScript();
                return;
            }
            if (getNumberOfNonCommentLinesBeforeFirstBlankLine() != _headerLineCount)
            {
                createProblemMessage_HeaderWrongSize();
                return;
            }
            var lineNumber:int = 0;
            var lineTypeId:String;
            var lineAnalyzer:LineTypeAnalyzer;
            for each (var lineString:String in _lineStringList)
            {
                lineNumber ++;
                lineTypeId = determineLineTypeId(lineNumber)
                lineAnalyzer = getLineAnalyzerInstanceForLineTypeId(lineTypeId);
                lineAnalyzer.lineNumber = lineNumber;
                lineAnalyzer.lineTypeId = lineTypeId;
                lineAnalyzer.lineText = lineString;
                _lineAnalyzerList.push(lineAnalyzer);
            }
        }

        private function analyzeLines_SetRoleInfo():void
        {
            var lineAnalyzer:LineTypeAnalyzer;
            var currentRoleName:String = null;
            var problemChunkLines_NoRole_List:Array = [];
            for (var i:int = 0; i < _lineCount; i++)
            {
                lineAnalyzer = _lineAnalyzerList[i];
                switch (lineAnalyzer.lineTypeId)
                {
                    case LineTypeConstants.LINE_TYPE_ID__ROLE_IDENTIFICATION:
                        // roleName was set in these lines in analyzeLines_CheckLines()
                        currentRoleName = LineTypeAnalyzer_RoleIdentification(lineAnalyzer).roleName;
                        break;
                    default:
                        lineAnalyzer.roleName = currentRoleName;
                }
                if (lineAnalyzer.bIsChunkLine)
                {
                    if ((!currentRoleName) && (!doesScriptUseDefaultRole()))
                    {
                        problemChunkLines_NoRole_List.push(lineAnalyzer);
                    }
                }
            }
            if (problemChunkLines_NoRole_List.length > 0)
            {
                createProblemMessage_ChunkLinesWithNoRole(problemChunkLines_NoRole_List);
            }
        }

        private function computeCodeCharacterCountForScript():int
        {
            var result:int = 0;
            for each (var chunkAnalyzer:ChunkAnalyzer in _chunkAnalyzerList)
            {
                result += chunkAnalyzer.lineCodeCharCount;
            }
            return result;
        }

        private function computeEnglishWordCountForScript():int
        {
            var result:int = 0;
            result += Utils_String.getWordCountForString(_lessonName);
            result += _releaseStatementEnglishWordCount;
            for each (var chunkAnalyzer:ChunkAnalyzer in _chunkAnalyzerList)
            {
                result += chunkAnalyzer.getLineTextWordCount(LineTypeConstants.LINE_TYPE_ID__CHUNK__NATIVE_LANGUAGE);
            }
            return result;
        }

        private function computeNumberOfChunkLineAnalyzersInList(list:Array):int
        {
            var result:int = 0;
            for each (var analyzer:LineTypeAnalyzer in list)
            {
                if (analyzer.bIsChunkLine)
                    result ++;
            }
            return result;
        }

        private function computePaymentChineseScriptRole(roleName:String):String
        {
            var payment:int = Math.ceil(computeSyllableCountForChineseRole(roleName) * _payment__chinese__yuan_per_syllable);
            var result:String = "￥" + String(payment);
            return result;
        }

        private function computePaymentEnglishScript():String
        {
            var wordPayment:Number = computeEnglishWordCountForScript() * _payment__english__dollars_per_word;
            var codeCharPayment:Number = computeCodeCharacterCountForScript() * _payment__english__dollars_per_code_char;
            var totalString:String = "$" + Utils_DataConversionComparison.convertNumberToString((wordPayment + codeCharPayment), 2);
            return totalString;
        }

        private function computeSyllableCountForChineseChunk(chunkAnalyzer:ChunkAnalyzer):int
        {
            var result:int;
            result += chunkAnalyzer.lineCodeSyllableCount;
            result += chunkAnalyzer.getLineText(LineTypeConstants.LINE_TYPE_ID__CHUNK__TARGET_LANGUAGE).length;
            return result;
        }

        private function computeSyllableCountForChineseRole(roleName:String):int
        {
            var result:int;
            result += Math.ceil(_lessonName.length / 2);
            result += Math.ceil(("Role: " + roleName).length / 2 );
            for each (var chunkAnalyzer:ChunkAnalyzer in _chunkAnalyzerList)
            {
                if (chunkAnalyzer.roleName == roleName)
                {
                    result += computeSyllableCountForChineseChunk(chunkAnalyzer);
                }
            }
            result += _releaseStatementHanziCharacterCount;
            return result;
        }

        private function constructFullProblemString(lineNumber:int, lineText:String, problemString:String):String
        {
            var result:String = "Line #: " + lineNumber + "\n\nLine Text: " + lineText + "\n\nProblem: " + problemString;
            return result;
        }

        private function createProblemMessage_CannotIdentifyTypeOfLine(lineNum:int):void
        {
            var problemString:String = "I can't tell what kind of line this line is. If it's a chunk line it should be in a group of at least " + getMinNonCommentChunkLines() + ", and no more than " + getMaxNonCommentChunkLines() + " lines, but this line isn't in a group like that. " + getNonplussedCommentString();
            problemString = constructFullProblemString(lineNum, _lineStringList[lineNum - 1], problemString);
            addProblemMessage(problemString);
        }

        private function createProblemMessage_EmptyScript():void
        {
            var problemString:String = "The script has no contents.";
            addProblemMessage(problemString);
        }

        private function createProblemMessage_HeaderWrongSize():void
        {
            var problemString:String = "This script's header has the wrong number of lines. Scripts should start with " + _headerLineCount + " header lines (not counting comment lines) then a blank line, but in this script there are " + getNumberOfNonCommentLinesBeforeFirstBlankLine() + " lines before the first blank line.";
            addProblemMessage(problemString);
        }

        private function createProblemMessage_ChunkLinesWithNoRole(lineAnalyzerList:Array):void
        {
            var text:String = "These lines appear to be chunk lines, and no role has been specified for them:\n";
            for each (var lineAnalyzer:LineTypeAnalyzer in lineAnalyzerList)
            {
                text += lineAnalyzer.lineNumber + ": " + lineAnalyzer.lineText + "\n";
            }
            addProblemMessage(text);
        }

        private function createProblemMessage_ScriptHasNoBlankLines():void
        {
            var text:String = "This script contains no blank lines.";
            addProblemMessage(text);
        }

        private function createResultMessage(messageText:String):void
        {
            _resultsList.push(messageText);
        }

        private function createVoiceScriptsInfo_CreateVoiceScriptsLanguageInfo_Chinese():VoiceScriptsLanguageInfo
        {
            var result:VoiceScriptsLanguageInfo = new VoiceScriptsLanguageInfo();
            var roleInfoList:Dictionary = new Dictionary();
            result.languageCode = LanguageCodeConstants.ISO_639_3_CODE_CHINESE_MANDARIN;
            result.roleInfoList = roleInfoList;
            for each (var roleName:String in roleList)
            {
                roleInfoList[roleName] = createVoiceScriptsInfo_CreateVoiceScriptsRoleInfo_Chinese(roleName);
            }
            return result;
        }

        private function createVoiceScriptsInfo_CreateVoiceScriptsLanguageInfo_English():VoiceScriptsLanguageInfo
        {
            var result:VoiceScriptsLanguageInfo = new VoiceScriptsLanguageInfo();
            var roleInfoList:Dictionary = new Dictionary();
            result.languageCode = LanguageCodeConstants.ISO_639_3_CODE_ENGLISH;
            result.roleInfoList = roleInfoList;
            roleInfoList[MiscConstants.ROLE_ALL] = createVoiceScriptsInfo_CreateVoiceScriptsRoleInfo_English();
            return result;
        }

        private function createVoiceScriptsInfo_CreateVoiceScriptsRoleInfo_Chinese(roleName:String):VoiceScriptsRoleInfo
        {
            var result:VoiceScriptsRoleInfo = new VoiceScriptsRoleInfo();
            result.roleName = roleName;
            result.buttonLabel = (roleName == "Default") ? "Default Role" : "Role: " + roleName;
            var voiceScriptText:String = "";
            voiceScriptText += createVoiceScriptsInfo_CreateVoiceScriptsRoleInfo_Instructions() + "\n";
            voiceScriptText += "[ Total Syllables: " + computeSyllableCountForChineseRole(roleName) + " ]\n";
            voiceScriptText += "[ Payment Rate: " + _payment__chinese__yuan_per_syllable + " Yuan per syllable ]\n";
            voiceScriptText += "[ Total Payment: " + computePaymentChineseScriptRole(roleName) + " ]\n";
            voiceScriptText += "\n\n\n";
            voiceScriptText += "++ " + _lessonName + " ++\n";
            voiceScriptText += "++ Role: " + roleName + " ++\n";
            voiceScriptText += "\n";
            var currChunkRoleName:String;
            for each (var chunkAnalyzer:ChunkAnalyzer in _chunkAnalyzerList)
            {
                if (currChunkRoleName != chunkAnalyzer.roleName)
                {
                    voiceScriptText += "[ " + chunkAnalyzer.roleName + " ]\n\n";
                    currChunkRoleName = chunkAnalyzer.roleName;
                }
                voiceScriptText += "[ " + chunkAnalyzer.getLineText(LineTypeConstants.LINE_TYPE_ID__CHUNK__NATIVE_LANGUAGE) + " ]\n";
                voiceScriptText += "[ " + chunkAnalyzer.getLineText(LineTypeConstants.LINE_TYPE_ID__CHUNK__TARGET_LANGUAGE__ROMANIZED) + " ]\n";
                if (chunkAnalyzer.roleName == roleName)
                {
                    voiceScriptText += "[ " + computeSyllableCountForChineseChunk(chunkAnalyzer) + " ]\n";
                    voiceScriptText += "++ " + Utils_String.padBeginning(String(chunkAnalyzer.chunkNumber), 2, "0") + " ++\n";
                    voiceScriptText += chunkAnalyzer.getLineText(LineTypeConstants.LINE_TYPE_ID__CHUNK__TARGET_LANGUAGE) + "\n";
                }
                else
                {
                    voiceScriptText += "[ " + chunkAnalyzer.getLineText(LineTypeConstants.LINE_TYPE_ID__CHUNK__TARGET_LANGUAGE) + " ]\n";
                }
                voiceScriptText += "\n";
            }
            voiceScriptText += "\n\n";
            // wo2 tong2yi4, yi4jing1 fu4kuan3 ￥______, lu4yin1 chan3pin3 jiang1 hui4 zai4 
            // chuang4zuo4 gong4yong4 "shu3ming2-xiang1tong2 fang1shi4 gong4xiang3 2.5 tong1yong4" xia4 shou4quan2 fa1xing2.
            voiceScriptText += "++  我同意，一经付款 " + computePaymentChineseScriptRole(roleName) + "，录音产品将会在     ++\n";
            voiceScriptText += '++  创作共用"署名-相同方式共享 2.5 通用"下授权发行。  ++\n';
            voiceScriptText += "\n\n\n\n\n\n";
            result.voiceScriptText = voiceScriptText;
            return result;
        }

        private function createVoiceScriptsInfo_CreateVoiceScriptsRoleInfo_English():VoiceScriptsRoleInfo
        {
            var result:VoiceScriptsRoleInfo = new VoiceScriptsRoleInfo();
            result.roleName = MiscConstants.ROLE_ALL;
            result.buttonLabel = "Create 'All Roles' Script";
            var voiceScriptText:String = "";
            voiceScriptText += createVoiceScriptsInfo_CreateVoiceScriptsRoleInfo_Instructions() + "\n";
            voiceScriptText += "++ " + _lessonName + " ++\n\n";
            voiceScriptText += "[ Total Words: " + computeEnglishWordCountForScript() + " ]\n";
            voiceScriptText += "[ Total Code Characters: " + computeCodeCharacterCountForScript() + " ]\n";
            voiceScriptText += "[ Payment Per Word: $" + _payment__english__dollars_per_word + " ]\n";
            voiceScriptText += "[ Payment Per Code Character: $" + _payment__english__dollars_per_code_char + " ]\n";
            voiceScriptText += "[ Total Payment: " + computePaymentEnglishScript() + " ]\n";
            voiceScriptText += "\n";
            var currChunkRoleName:String;
            for each (var chunkAnalyzer:ChunkAnalyzer in _chunkAnalyzerList)
            {
                if (currChunkRoleName != chunkAnalyzer.roleName)
                {
                    voiceScriptText += "[ " + chunkAnalyzer.roleName + " ]\n\n";
                    currChunkRoleName = chunkAnalyzer.roleName;
                }
                var wordCount:int = chunkAnalyzer.getLineTextWordCount(LineTypeConstants.LINE_TYPE_ID__CHUNK__NATIVE_LANGUAGE);
                voiceScriptText += "[ " + chunkAnalyzer.lineCodeCharCount + " code characters, " + wordCount + " words ]\n";
                voiceScriptText += "++ " + Utils_String.padBeginning(String(chunkAnalyzer.chunkNumber), 2, "0") + " ++\n";
                for each (var lineAnalyzer:LineTypeAnalyzer in chunkAnalyzer.lineAnalyzerListIncludingCommentLines)
                {
                    if (lineAnalyzer is LineTypeAnalyzer_Comment)
                    {
                        if (LineTypeAnalyzer_Comment(lineAnalyzer).isVoiceScriptNoteCommentForLanguage(LanguageCodeConstants.ISO_639_3_CODE_ENGLISH))
                        {
                            var voiceScriptComment:String = LineTypeAnalyzer_Comment(lineAnalyzer).extractVoiceScriptNoteFromComment();
                            voiceScriptText += "[  NOTE: " + voiceScriptComment + "  ]\n";
                        }
                    }
                }
                voiceScriptText += chunkAnalyzer.getLineText(LineTypeConstants.LINE_TYPE_ID__CHUNK__NATIVE_LANGUAGE) + "\n";
                voiceScriptText += "\n";
            }
            voiceScriptText += "\n\n";
            voiceScriptText += "++  I agree that, effective upon payment of " + computePaymentEnglishScript() + " to me, this recording is ++\n";
            voiceScriptText += "++  irrevocably released by me under the Creative Commons Attribution-Sharealike license.  ++\n";
            voiceScriptText += "\n\n\n\n\n\n";
            result.voiceScriptText = voiceScriptText;
            return result;
        }

        private function createVoiceScriptsInfo_CreateVoiceScriptsRoleInfo_Instructions():String
        {
            var result:String = "";
            result += "Instructions:\n";
            result += "\n";
            result += "1. Content that isn't enclosed in [ square brackets ] or ++ plus signs ++:\n";
            result += "    - This is content which will be used in learning modules.\n";
            result += "    - Please record this content.\n";
            result += "    - Recorded audio for this content should meet high quality standards.\n";
            result += "\n";
            result += "2. Content that is enclosed in ++ plus signs ++:\n";
            result += "    - This is informational content which won't be used in learning modules but\n";
            result += "      will be used by us for other purposes.\n";
            result += "    - Please record this content.\n";
            result += "    - Recorded audio for this content doesn't need to meet high quality\n";
            result += "      standards - it just needs to be easy to hear and understand.\n";
            result += "    - Some of this content consists of 'line codes' containing a number of letters\n";
            result += '      and numbers, for example, "mrup001". These codes should be "spelled out" in\n';
            result += "      English, with each letter and number spoken separatedly. Please pronounce\n";
            result += '      the zero (0) as "zero".\n';
            result += "    - This content also includes a 'release statement' at the end of the script, in\n";
            result += "      which you affirm that, upon payment of the offered amount, you are releasing\n";
            result += "      the recorded content under a Creative Commons license.\n";
            result += "\n";
            result += "3. Content that is enclosed in [ square brackets ]:\n";
            result += "    - This content is information that may be helpful to you.\n";
            result += "    - Please *don't* record this content.\n";
            result += "    - This includes word counts for each line, lines that you don't need to\n";
            result += "      read, etc.\n";
            result += "    - Please pay special attention to comments that start with 'NOTE:'.\n";
            result += "\n";
            result += "Thanks!  :)\n\n\n\n\n\n";
            return result;
        }

        private function createXML_ChunksElement():XML
        {
            var result:XML = <chunks/>;
            for each (var chunkAnalyzer:ChunkAnalyzer in _chunkAnalyzerList)
            {
                var chunkElement:XML = <chunk/>;
                var chunkNumberString:String = Utils_DataConversionComparison.convertNumberToString(chunkAnalyzer.chunkNumber, 0, true, 2, "0");
                var fileNameRootElement:XML = <fileNameRoot>{chunkNumberString}</fileNameRoot>;
                var textNativeLanguageElement:XML = <textNativeLanguage>{chunkAnalyzer.getLineText(LineTypeConstants.LINE_TYPE_ID__CHUNK__NATIVE_LANGUAGE)}</textNativeLanguage>;
                var textTargetLanguagePhonetic:XML = <textTargetLanguagePhonetic>{chunkAnalyzer.getLineText(LineTypeConstants.LINE_TYPE_ID__CHUNK__TARGET_LANGUAGE__ROMANIZED)}</textTargetLanguagePhonetic>;
                chunkElement.appendChild(fileNameRootElement);
                chunkElement.appendChild(textNativeLanguageElement);
                chunkElement.appendChild(textTargetLanguagePhonetic);
                result.appendChild(chunkElement);
            }
            return result;
        }

        private function determineLineTypeId(lineNum:int):String
        {
            if (lineNum == 45)
            {
                var i:int = 0;
            }
            var result:String;
            if (isLineBlankLine(lineNum))
            {
                result = LineTypeConstants.LINE_TYPE_ID__BLANK;
            }
            else if (isLineCommentLine(lineNum))
            {
                result = LineTypeConstants.LINE_TYPE_ID__COMMENT;
            }
            else if (isLineRoleLine(lineNum))
            {
                result = LineTypeConstants.LINE_TYPE_ID__ROLE_IDENTIFICATION;
            }
            else if (isLineHeaderLine(lineNum))
            {
                result = determineLineType_HeaderLine(lineNum);
            }
            else if (isLineChunkLine(lineNum))
            {
                result = determineLineType_ChunkLine(lineNum);
            }
            else
            {
                createProblemMessage_CannotIdentifyTypeOfLine(lineNum);
                result = LineTypeConstants.LINE_TYPE_ID__UNKNOWN;
            }
            return result;
        }

        private function determineLineType_ChunkLine(lineNum:int):String
        {
            var result:String;
            var positionInChunk:int = getPositionOfLineInChunk(lineNum);
            switch (positionInChunk)
            {
                case 1:
                    result = LineTypeConstants.LINE_TYPE_ID__CHUNK__TARGET_LANGUAGE__ROMANIZED;
                    break;
                case 2:
                    result = LineTypeConstants.LINE_TYPE_ID__CHUNK__TARGET_LANGUAGE;
                    break;
                case 3:
                    result = LineTypeConstants.LINE_TYPE_ID__CHUNK__NATIVE_LANGUAGE;
                    break;
                case 4:
                    result = LineTypeConstants.LINE_TYPE_ID__CHUNK__NOTE;
                    break;
                default:
                    Log.fatal("ScriptAnalyzer.determineLineType_ChunkLine(): no match for positionInChunk of " + positionInChunk);
            }
            return result;
        }

        private function determineLineType_HeaderLine(lineNum:int):String
        {
            var result:String;
            var commentLineCount:int = getNumberOfCommentLinesBeforeLine(lineNum);
            switch (lineNum - commentLineCount)
            {
                case _headerLineNumber_AuthorName:
                    result = LineTypeConstants.LINE_TYPE_ID__HEADER__AUTHOR_NAME;
                    break;
                case _headerLineNumber_LessonId:
                    result = LineTypeConstants.LINE_TYPE_ID__HEADER__LESSON_ID;
                    break;
                case _headerLineNumber_LessonName:
                    result = LineTypeConstants.LINE_TYPE_ID__HEADER__LESSON_NAME;
                    break;
                case _headerLineNumber_LessonSortName:
                    result = LineTypeConstants.LINE_TYPE_ID__HEADER__LESSON_SORT_NAME;
                    break;
                case _headerLineNumber_Level:
                    result = LineTypeConstants.LINE_TYPE_ID__HEADER__LEVEL;
                    break;
                case _headerLineNumber_LibraryId:
                    result = LineTypeConstants.LINE_TYPE_ID__HEADER__LIBRARY_ID;
                    break;
                case _headerLineNumber_NativeLanguage:
                    result = LineTypeConstants.LINE_TYPE_ID__HEADER__NATIVE_LANGUAGE;
                    break;
                case _headerLineNumber_ProviderId:
                    result = LineTypeConstants.LINE_TYPE_ID__HEADER__PROVIDER_ID;
                    break;
                case _headerLineNumber_ReleaseType:
                    result = LineTypeConstants.LINE_TYPE_ID__HEADER__RELEASE_TYPE;
                    break;
                case _headerLineNumber_Roles:
                    result = LineTypeConstants.LINE_TYPE_ID__HEADER__ROLES;
                    break;
                case _headerLineNumber_TargetLanguage:
                    result = LineTypeConstants.LINE_TYPE_ID__HEADER__TARGET_LANGUAGE;
                    break;
                default:
                    Log.fatal("ScriptAnalyzer.determineLineType_HeaderLine(): no match for lineNum of " + lineNum);
            }
            return result;
        }

        private function doesScriptHaveAnyBlankLines():Boolean
        {
            var result:Boolean = false;
            var lineText:String;
            for (var lineNum:int = 1; lineNum <= _lineStringList.length; lineNum ++)
            {
                lineText = _lineStringList[lineNum - 1];
                if (LineTypeAnalyzer_Blank.isLineThisType(lineText))
                {
                    result = true;
                    break;
                }
            }
            return result;
        }

        private function doesScriptUseDefaultRole():Boolean
        {
            return (roleList[0] == MiscConstants.ROLE_DEFAULT);
        }

        private function getCountOfCommentLinesInLineGroup(firstLineNum:int, lastLineNum:int):int
        {
            var result:int = 0;
            if (!lastLineNum > firstLineNum)
                Log.fatal("ScriptAnalyzer.getCountOfCommentLinesInLineGroup(): lastLineNum must be greater than firstLineNum");
            for (var i:int = firstLineNum - 1; i < lastLineNum; i++)
            {
                if (isLineCommentLine(i + 1))
                    result ++;
            }
            return result;
        }

        private function getFirstBlankLineNumAfter(lineNum:int):int
        {
            // Next 2 lines are silly but, hopefully, explanatory
            var firstLineNumToCheck:int = lineNum + 1;
            var firstIndexToCheck:int = firstLineNumToCheck - 1;
            for (var i:int = firstIndexToCheck; i < _lineStringList.length; i++)
            {
                if (LineTypeAnalyzer_Blank.isLineThisType(_lineStringList[i]))
                {
                    return i + 1;
                }
            }
            // We didn't find a blank line so we return lineCount + 1.
            // In other words, for the purposes of our logic, the nonexistent line
            // after the scripts final line is considered to be "blank".
            return _lineStringList.length + 1;
        }

        private function getLastBlankLineNumBefore(lineNum:int):int
        {
            // We return -1 if there is no previous blank line.
            if (lineNum < 2)
                return -1;
            var firstLineNumToCheck:int = lineNum - 1;
            var firstIndexToCheck:int = firstLineNumToCheck - 1;
            for (var i:int = firstIndexToCheck; i >= 0; i--)
            {
                if (LineTypeAnalyzer_Blank.isLineThisType(_lineStringList[i]))
                {
                    return i + 1;
                }
            }
            return -1;
        }

        private function getLineAnalyzerInstanceForLineTypeId(lineTypeId:String):LineTypeAnalyzer
        {
            var clazz:Class = _lineAnalyzerClassIndex[lineTypeId];
            var result:LineTypeAnalyzer = new clazz();
            return result;
        }

        private function getLineAnalyzerForLineNumberNotCountingCommentLines(lineNum:int):LineTypeAnalyzer
        {
            var result:LineTypeAnalyzer;
            var currNonCommentLineNum:int = 0;
            var currAnalyzer:LineTypeAnalyzer;
            for (var tempLineNum:int = 1; tempLineNum <= _lineAnalyzerList.length; tempLineNum ++)
            {
                currAnalyzer = _lineAnalyzerList[tempLineNum - 1];
                if (!(currAnalyzer is LineTypeAnalyzer_Comment))
                {
                    currNonCommentLineNum ++;
                    if (currNonCommentLineNum == lineNum)
                    {
                        result = _lineAnalyzerList[tempLineNum - 1];
                        break;
                    }
                }
            }
            if (!result)
                Log.fatal("ScriptAnalyzer.getLineAnalyzerForLineNumberNotCountingCommentLines(): Script doesn't contain " + lineNum + " non-comment lines.");
            return result;
        }

        private function getMinNonCommentChunkLines():int
        {
            return 2 + _nativeLanguageCount;
        }

        private function getMaxNonCommentChunkLines():int
        {
            return 3 + _nativeLanguageCount;
        }

        private function getNonplussedCommentString():String
        {
            var possibleCommentList:Array = [];
            possibleCommentList.push("I'm not a very smart piece of software so I just can't figure this out.");
            possibleCommentList.push("Being a Script Helper of very little brain, this leaves me in a state of muddled confusion.");
            possibleCommentList.push("I don't understand this, but I'm only a computer program. Perhaps if you apply your human brain to this situation you'll be able to figure out what the problem is.");
            possibleCommentList.push("I don't have a clue as to why this is happening. Can you take over from here?");
            var randomChoice:int = Math.floor(3.9999 * Math.random());
            var result:String = possibleCommentList[randomChoice];
            return result;
        }

        private function getNumberOfCommentLinesBeforeLine(lineNum:int):int
        {
            var result:int = 0;
            var lineText:String;
            for (var tempLineNum:int = 1; tempLineNum <= lineNum; tempLineNum ++)
            {
                lineText = _lineStringList[tempLineNum - 1];
                if (LineTypeAnalyzer_Comment.isLineThisType(lineText))
                    result ++;
            }
            return result;
        }

        private function getNumberOfNonCommentLinesBeforeFirstBlankLine():int
        {
            var firstBlankLineNum:int = getFirstBlankLineNumAfter(0);
            if (firstBlankLineNum == 0)
                return 0;
            var result:int = (firstBlankLineNum - 1) - getNumberOfCommentLinesBeforeLine(firstBlankLineNum);
            return result;
        }

        private function getPositionOfLineInChunk(lineNum:int):int
        {
            if (!isLineChunkLine(lineNum))
                Log.fatal("ScriptAnalyzer.getPositionOfLineInChunk(): line isn't chunk line");
            var previousBlankLineNum:int = getLastBlankLineNumBefore(lineNum);
            if (previousBlankLineNum == -1)
                Log.fatal("ScriptAnalyzer.getPositionOfLineInChunk(): no previous blank line exists");
            if (previousBlankLineNum >= lineNum)
                Log.fatal("ScriptAnalyzer.getPositionOfLineInChunk(): previous blank line is greater than this line (?)");
            var commentLineCount:int = getCountOfCommentLinesInLineGroup(previousBlankLineNum + 1, lineNum - 1);
            var result:int = (lineNum - previousBlankLineNum) - commentLineCount;
            return result;
        }

        private function initProps():void
        {
            _lineAnalyzerClassIndex = new Dictionary();
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__BLANK]                              = LineTypeAnalyzer_Blank;
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__CHUNK__NATIVE_LANGUAGE]             = LineTypeAnalyzer_Chunk_NativeLanguage;
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__CHUNK__NOTE]                        = LineTypeAnalyzer_Chunk_Note;
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__CHUNK__TARGET_LANGUAGE]             = LineTypeAnalyzer_Chunk_TargetLanguage;
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__CHUNK__TARGET_LANGUAGE__ROMANIZED]  = LineTypeAnalyzer_Chunk_TargetLanguage_Romanized;
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__COMMENT]                            = LineTypeAnalyzer_Comment;
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__HEADER__AUTHOR_NAME]                = LineTypeAnalyzer_Header_AuthorName;
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__HEADER__LESSON_ID]                  = LineTypeAnalyzer_Header_LessonId;
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__HEADER__LESSON_NAME]                = LineTypeAnalyzer_Header_LessonName;
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__HEADER__LESSON_SORT_NAME]           = LineTypeAnalyzer_Header_LessonSortName;
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__HEADER__LEVEL]                      = LineTypeAnalyzer_Header_Level;
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__HEADER__LIBRARY_ID]                 = LineTypeAnalyzer_Header_LibraryId;
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__HEADER__NATIVE_LANGUAGE]            = LineTypeAnalyzer_Header_NativeLanguage;
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__HEADER__PROVIDER_ID]                = LineTypeAnalyzer_Header_ProviderId;
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__HEADER__RELEASE_TYPE]               = LineTypeAnalyzer_Header_ReleaseType;
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__HEADER__ROLES]                      = LineTypeAnalyzer_Header_Roles;
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__HEADER__TARGET_LANGUAGE]            = LineTypeAnalyzer_Header_TargetLanguage;
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__ROLE_IDENTIFICATION]                = LineTypeAnalyzer_RoleIdentification;
            _lineAnalyzerClassIndex[LineTypeConstants.LINE_TYPE_ID__UNKNOWN]                            = LineTypeAnalyzer_Chunk_NativeLanguage;
        }

        private function isLineBlankLine(lineNum:int):Boolean
        {
            return LineTypeAnalyzer_Blank.isLineThisType(_lineStringList[lineNum - 1]);
        }

        private function isLineChunkLine(lineNum:int):Boolean
        {
            // Assumption: We're past the header.
            // For purposes of this initial line type id we simply confirm that this line is part of a group of lines bordered by
            // blank lines, where the number of non-comment lines in the group equals ((2 or 3) + _nativeLanguageCount).
            if (isLineBlankLine(lineNum))
                return false;
            var previousBlankLineNum:int = getLastBlankLineNumBefore(lineNum);
            if (previousBlankLineNum == -1)
                return false;
            var nextBlankLineNum:int = getFirstBlankLineNumAfter(lineNum);
            var linesInGroup:int = (nextBlankLineNum - previousBlankLineNum) - 1;
            var commentLinesInGroup:int = getCountOfCommentLinesInLineGroup(previousBlankLineNum + 1, nextBlankLineNum - 1);
            var nonCommentLinesInGroup:int = linesInGroup - commentLinesInGroup;
            if ((nonCommentLinesInGroup < getMinNonCommentChunkLines()) || (nonCommentLinesInGroup > getMaxNonCommentChunkLines()))
                return false;
            return true;
        }

        private function isLineCommentLine(lineNum:int):Boolean
        {
            return LineTypeAnalyzer_Comment.isLineThisType(_lineStringList[lineNum - 1])
        }

        private function isLineHeaderLine(lineNum:int):Boolean
        {
            // Assumption: Out code has already established that the first blank line is in
            // an appropriate place, i.e. we have an appropriate number of header lines.
            return (lineNum < getFirstBlankLineNumAfter(0));
        }

        private function isLineIgnoreProblemsCommentLine(lineNum:int):Boolean
        {
            if (lineNum == 0)
                return false;
            if (!LineTypeAnalyzer_Comment.isLineThisType(_lineStringList[lineNum - 1], _lineStringList, lineNum))
                return false;
            var result:Boolean = (String(_lineStringList[lineNum - 1]).indexOf(MiscConstants.IGNORE_PROBLEMS_TAG) != -1);
            return result;
        }

        private function isLineRoleLine(lineNum:int):Boolean
        {
            return LineTypeAnalyzer_RoleIdentification.isLineThisType(_lineStringList[lineNum - 1], _lineStringList, lineNum);
        }

        private function populateLessonNameProp():void
        {
            _lessonName = LineTypeAnalyzer_Header_LessonName(getLineAnalyzerForLineNumberNotCountingCommentLines(_headerLineNumber_LessonName)).lineText;
        }

        private function populateRolesListProp():void
        {
            roleList = LineTypeAnalyzer_Header_Roles(getLineAnalyzerForLineNumberNotCountingCommentLines(_headerLineNumber_Roles)).getRolesList();
        }

        private function reportResults():void
        {
            var tempResults:String = "";
            if (_problemsList.length > 0)
            {
                tempResults = _problemsList.length + " problem(s) were found in this script. Please correct these problems, then try again.\n\n"
                for (var i:int = 0; i < _problemsList.length; i++)
                {
                    var problemString:String = _problemsList[i];
                    tempResults += "Problem #" + (i + 1) + ":\n\n" + problemString + "\n\n\n";
                }
            }
            else
            {
                tempResults = "No Problems Found!  :)";
            }
            if (_resultsList.length > 0)
            {
                for each (var resultString:String in _resultsList)
                {
                    if (tempResults.length > 0)
                        tempResults += "\n\n\n";
                    tempResults += resultString;
                }
            }
            _resultsString = tempResults;
        }
    }
}
